package com.geetour.columbus.common.utils;

import java.math.BigDecimal;

/**
 * @author ChenKai
 *
 */
public final class Mathematics {

	/**
	 * Add two integers, checking for overflow.
	 *
	 * @param x an operand.
	 * @param y an operand.
	 * @return the sum <i>x + y</i>.
	 * @throws ArithmeticException if the result can not be represented as an int.
	 */
	public static int add(int x, int y) {
		long s = (long)x + (long)y;
        if(s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) {
            throw new ArithmeticException("overflow: add.");
        }
        return (int)s;
	}

	/**
	 * Add two long integers, checking for overflow.
	 *
	 * @param x an operand.
	 * @param y an operand.
	 * @return the sum <i>x + y</i>.
	 * @throws ArithmeticException if the result can not be represented as a long.
	 */
	public static long add(long x, long y) {
		return add(x, y, "overflow: add.");
	}

	/**
	 * Add two long integers, checking for overflow.
	 *
	 * @param x an operand.
	 * @param y an operand.
	 * @param msg the message to use for any thrown exception.
	 * @return the sum <i>x + y</i>.
	 * @throws ArithmeticException if the result can not be represented as a long.
	 */
	public static long add(long x, long y, String msg) {
		if(x > y)
			return add(y, x, msg);

		// assert x <= y;
		if(x < 0) {
			if(y < 0) {
				// assert x < 0 && y < 0;
				if(Long.MIN_VALUE - y <= x) {
					return x + y;
				} else {
					throw new ArithmeticException(msg);
				}
			} else {
				// assert x < 0 && y >= 0;
				return x + y;
			}
		} else {
			// assert x >=0 && y >= x;
			if(x <= Long.MAX_VALUE - y) {
				return x + y;
			} else {
				throw new ArithmeticException(msg);
			}
		}
	}

	/**
	 * Subtract two integers, checking for overflow.
	 *
	 * @param x the minus-end.
	 * @param y the subtrahend.
	 * @return the difference <i>x - y</i>.
	 * @throws ArithmeticException if the result can not be represented as an int.
	 */
	public static int subtract(int x, int y) {
		long d = (long)x - (long)y;
        if(d < Integer.MIN_VALUE || d > Integer.MAX_VALUE) {
            throw new ArithmeticException("overflow: subtract.");
        }
        return (int)d;
	}

	/**
	 * Subtract two long integers, checking for overflow.
	 *
	 * @param x the minus-end.
	 * @param y the subtrahend.
	 * @return the difference <i>x - y</i>.
	 * @throws ArithmeticException if the result can not be represented as a long.
	 */
	public static long subtract(long x, long y) {
		String msg = "overflow: subtract.";
		if (y == Long.MIN_VALUE) {
            if (x < 0) {
                return x - y;
            } else {
                throw new ArithmeticException(msg);
            }
        } else {
        	return add(x, -y, msg);
        }
	}

	/**
	 * Multiply two integers, checking for overflow.
	 *
	 * @param x a factor.
	 * @param y a factor.
	 * @return the product <i>x * y</i>.
	 */
	public static int multiply(int x, int y) {
		long p = (long)x * (long)y;
		if(p < Integer.MIN_VALUE || p > Integer.MAX_VALUE) {
			throw new ArithmeticException("overflow: multiply.");
		}

		return (int)p;
	}

	/**
	 * Multiply two long integers, checking for overflow.
	 *
	 * @param x a factor.
	 * @param y a factor.
	 * @return the product <i>x * y</i>.
	 */
	public static long multiply(long x, long y) {
		if(x > y) {
			return multiply(y, x);
		}

		String msg = "overflow: multiply.";
		// assert x <= y;
		if(x < 0) {
			if(y < 0) {
				// assert x < 0 && y < 0;
				if(x >= Long.MAX_VALUE / y) {
					return x * y;
				} else {
					throw new ArithmeticException(msg);
				}
			} else if(y > 0) {
				// assert x < 0 && y > 0;
				if(x >= Long.MIN_VALUE / y) {
					return x * y;
				} else {
					throw new ArithmeticException(msg);
				}
			} else {
				// assert x < 0 && y == 0;
				return 0;
			}
		} else {
			if (x > 0) {
				// assert x > 0 && y >= x;
				if(x <= Long.MAX_VALUE / y) {
					return x * y;
				} else {
					throw new ArithmeticException(msg);
				}
			} else {
				// assert x == 0 && y >= x;
				return 0;
			}
		}
	}

	/**
     * Returns true if both arguments are NaN or neither is NaN and they are
     * equal
     *
     * @param x first value
     * @param y second value
     * @return true if the values are equal or both are NaN
     */
    public static boolean equals(double x, double y) {
        return ((Double.isNaN(x) && Double.isNaN(y)) || x == y);
    }

	/**
	 * Round by the mode of ROUND_HALF_UP. <br><br><i>
	 * Mathematics.round(1.23);		// --> 1 <br>
	 * Mathematics.round(4.56);		// --> 5 <br></i>
	 *
	 * @param d the decimal you want to round.
	 * @return the integer rounded from <i>d</i>.
	 */
	public static long round(double d) {
		return round(d, BigDecimal.ROUND_HALF_UP);
	}

	/**
	 * Round by the mode of <i>roundingMode</i>.
	 *
	 * @param d the decimal you want to round.
	 * @param roundingMode the rounding mode.
	 * @return the integer rounded from <i>d</i> by the mode of <i>roundingMode</i>.
	 */
	public static long round(double d, int roundingMode) {
		return round(new BigDecimal(Double.toString(d)), roundingMode);
	}

	/**
	 * Round by the mode of <i>roundingMode</i>.
	 *
	 * @param d the decimal you want to round.
	 * @param roundingMode the rounding mode.
	 * @return the integer rounded from <i>d</i> by the mode of <i>roundingMode</i>.
	 */
	public static long round(BigDecimal d, int roundingMode) {
		return d.setScale(0, roundingMode).longValue();
	}

	/**
	 * Round by the mode of ROUND_HALF_UP. <br><br><i>
	 * Mathematics.round(1.234, 2);		// --> 1.23 <br>
	 * Mathematics.round(4.567, 2);		// --> 4.57 <br></i>
	 *
	 * @param d the decimal you want to round.
	 * @param scale the scale of the rounding result.
	 * @return the decimal rounded from <i>d</i> with the <i>scale</i>.
	 */
	public static double roundDecimal(double d, int scale) {
		return roundDecimal(d, scale, BigDecimal.ROUND_HALF_UP);
	}

	/**
	 * Round by the mode of <i>roundingMode</i>.
	 *
	 * @param d the decimal you want to round.
	 * @param scale the scale of the rounding result.
	 * @param roundingMode the rounding mode.
	 * @return the decimal rounded from <i>d</i> with the <i>scale</i> by the mode of <i>roundingMode</i>.
	 */
	public static double roundDecimal(double d, int scale, int roundingMode) {
		return roundDecimal(new BigDecimal(Double.toString(d)), scale, roundingMode);
	}

	/**
	 * Round by the mode of <i>roundingMode</i>.
	 *
	 * @param d the decimal you want to round.
	 * @param scale the scale of the rounding result.
	 * @param roundingMode the rounding mode.
	 * @return the decimal rounded from <i>d</i> with the <i>scale</i> by the mode of <i>roundingMode</i>.
	 */
	public static double roundDecimal(BigDecimal d, int scale, int roundingMode) {
		return d.setScale(scale, roundingMode).doubleValue();
	}
}